﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Collections;

namespace Manoso.Data.FluentMap
{
    internal interface IRelation
    {
        IList<MemberInfo> Ids { get; }
        IEnumerable<object> GetChildren(Type parentType, object parent);
        object Bind(object parent, object child);
    }

    internal abstract class Relation : IRelation
    {
        protected MemberInfo _member;
        protected Type _memberType;
        public IList<MemberInfo> Ids { get; private set; }


        protected void Create(LambdaExpression lambda)
        {
            Ids = new List<MemberInfo>();
            _member = lambda.GetMemberInfo<MemberInfo>();
            _memberType = _member.GetMemberType();
        }

        protected void Key(LambdaExpression idLambda)
        {
            var member = idLambda.GetMemberInfo<MemberInfo>();

            if (member is PropertyInfo || member is FieldInfo)
                Ids.Add(member);
        }

        public virtual IEnumerable<object> GetChildren(Type parentType, object parent)
        {
            if (parent == null || !parentType.IsInstanceOfType(parent)) return null;

            var result = _member.GetValue(parent);
            if (result == null) return null;

            var results = result as IEnumerable;
            if (results == null) return new[] {result};

            var dictionary = result as IDictionary;
            if (dictionary != null)
                results = dictionary.Values;
            return results.OfType<object>();
        }

        public abstract object Bind(object parent, object child);
    }

    internal interface IRelation<out T, TP> : IRelation
    {
        IRelation<T, TP> Key<TId>(Expression<Func<TP, TId>> idLambda);
    }

    internal class Relation<T, TP> : Relation, IRelation<T, TP>
    {
        private Action<object, object> Add { get; set; }
        private Func<object, object, object> Get { get; set; }

        public Relation()
        {
            Add = (x, y) => { };
            Get = (x, y) => x;
        }

        public Relation(Expression<Func<T, TP>> lambda)
        {
            Create(lambda);
            Add = _member.SetValue;
            Get = (x, y) =>
            {
                var value = _member.GetValue(x);
                if (value != null) return value;
                _member.SetValue(x, y);
                return y;
            }; 
        }

        public Relation(Expression<Func<T, TP[]>> lambda)
        {
            Create(lambda);
            Add = (x, y) =>
            {
                var subs = _member.GetValue(x) as TP[];
                var news = new[] { (TP)y };
                subs = subs == null ? news : subs.Concat(news).ToArray();
                _member.SetValue(x, subs);
            };
            Get = GetLots;
        }

        public Relation(Expression<Func<T, IList<TP>>> lambda)
        {
            Create(lambda);
            Add = (x, y) =>
            {
                var subs = _member.GetValue(x) as IList<TP> ?? new List<TP>();
                subs.Add((TP)y);
                _member.SetValue(x, subs);
            };
            Get = GetLots;
        }

        public Relation(Expression<Func<T, IEnumerable>> lambda, Action<T, TP> add, Action<T> create)
        {
            Create(lambda);
            if (add != null)
                Add = (x, y) =>
                {
                    var t = (T)x;
                    var sub = (TP)y;

                    var subs = _member.GetValue(x);
                    if (subs == null && create != null) create(t);
                    add(t, sub);
                };
            Get = GetLots;
        }

        public virtual IRelation<T, TP> Key<TId>(Expression<Func<TP, TId>> idLambda)
        {
            base.Key(idLambda);
            return this;
        }

        public override object Bind(object parent, object child)
        {
            var p = (T)parent;
            var s = (TP)child;

            var result = Get(parent, child);
            if (result != null) return result;

            Add(p, s);
            return child;
        }

        private object GetLots(object parent, object sub)
        {
            if (Ids == null || Ids.Count == 0)
                return null;

            var lots = _member.GetValue(parent) as IEnumerable;
            var items = lots as IDictionary;
            if (items != null)
                lots = items.Values;
            return lots == null ? null : lots.Cast<object>().FirstOrDefault(item => Equal(item, sub));
        }

        private bool Equal(object item, object target)
        {
            var itemIds = GetIds(item);
            var targetIds = GetIds(target);
            return itemIds != null && itemIds.Zip(targetIds, (i, t) => i.Equals(t)).All(x => x);
        }

        private IEnumerable<object> GetIds(object dto)
        {
            var ids = new List<object>();
            if (Ids != null)
                ids.AddRange(Ids.Select(x => x.GetValue(dto)));
            return ids.Count > 0 ? ids.ToArray() : null;
        }
    }
}